/*
 * Sylvain BERTRAND <digital.ragnarok@gmail.com>
 * code protected by GNU GPL v3
 */
#include <stdio.h>
#include <stdlib.h>
#include <glib.h>
#include "signal.h"
struct t_signal_callback {
	t_signal_handler func;
	gpointer data;
};
static gboolean signal_prepare(GSource *source, gint *timeout);
static gboolean signal_check(GSource *source);
static gboolean signal_occurred(GSource *source, GSourceFunc callback, gpointer data);
static void sighandler(gint sig);
#define NUM_SIGNALS 99
static sigset_t all_signals_set;
static struct {
	guint installed;
	struct sigaction oldact;
} all_signals[NUM_SIGNALS];
static const gint core_signals[] = {
	SIGABRT,
	SIGSEGV,
	SIGFPE,
	SIGILL,
	SIGQUIT,
	SIGTRAP,
	SIGSYS,
	SIGBUS,
	SIGXCPU,
	SIGXFSZ
};

#define NUM_CORE_SIGNALS (gint)(sizeof(core_signals) / sizeof(core_signals[0]))
static GSourceFuncs source_funcs = {
	signal_prepare,
	signal_check,
	signal_occurred,
	NULL
};

static GSource *gsource = NULL;
static guint listeners = 0;
static gboolean signal_fired;
static guint signals_fired[NUM_SIGNALS];
static GSList *callbacks[NUM_SIGNALS];
void t_signal_listen(void)
{
	if(!listeners) {
		guint i;
		struct sigaction action;
		sigset_t sigset;
		sigfillset(&all_signals_set);
		sigemptyset(&sigset);
		action.sa_handler = sighandler;
		action.sa_mask = sigset;
		action.sa_flags = SA_NOCLDSTOP;
		for(i = 0; i < NUM_CORE_SIGNALS; ++i) {
			if(core_signals[i] != SIGABRT) {
				sigaction(core_signals[i], &action, &all_signals[core_signals[i]].oldact);
				all_signals[core_signals[i]].installed++;
			}
		}
		gsource = g_source_new(&source_funcs, sizeof(GSource));
		g_source_set_priority(gsource, G_PRIORITY_HIGH);
		g_source_attach(gsource, NULL);
	}
	++listeners;
}

static void remove_callback(gint sig, t_signal_handler func)
{
	GSList *it;
	gint i;
	g_return_if_fail(func != NULL);
	g_return_if_fail(sig >= 0 && sig <= NUM_SIGNALS);
	for(i = 0; i < NUM_CORE_SIGNALS; ++i)
		g_return_if_fail(sig != core_signals[i]);
	for(it = callbacks[sig]; it; it = g_slist_next(it)) {
		struct t_signal_callback *cb = it->data;
		if(cb->func == func) {
			g_assert(all_signals[sig].installed > 0);
			callbacks[sig] = g_slist_delete_link(callbacks[sig], it);
			g_slice_free(struct t_signal_callback, cb);
			all_signals[sig].installed--;
			if(!all_signals[sig].installed)
				sigaction(sig, &all_signals[sig].oldact, NULL);
			break;
		}
	}
}


void t_signal_stop(void)
{
	--listeners;
	if(!listeners) {
		gint i;
		GSList *it, *next;
		g_source_unref(gsource);
		gsource = NULL;
		for(i = 0; i < NUM_SIGNALS; ++i)
			for(it = callbacks[i]; it; it = next) {
				struct t_signal_callback *cb = it->data;
				next = g_slist_next(it);
				remove_callback(i, cb->func);
			}
		for(i = 0; i < NUM_CORE_SIGNALS; ++i) {
			if(all_signals[core_signals[i]].installed) {
				sigaction(core_signals[i], &all_signals[core_signals[i]].oldact, NULL);
				all_signals[core_signals[i]].installed--;
			}
		}
#		ifdef DEBUG
		for(i = 0; i < NUM_SIGNALS; ++i)
			g_assert(all_signals[i].installed == 0);
#		endif
	}
}

void t_signal_add_callback(gint sig, t_signal_handler func, gpointer data)
{
	struct t_signal_callback *cb;
	gint i;
	g_return_if_fail(func != NULL);
	g_return_if_fail(sig >= 0 && sig <= NUM_SIGNALS);
	for(i = 0; i < NUM_CORE_SIGNALS; ++i)
		g_return_if_fail(sig != core_signals[i]);
	cb = g_slice_new(struct t_signal_callback);
	cb->func = func;
	cb->data = data;
	callbacks[sig] = g_slist_prepend(callbacks[sig], cb);
	if(!all_signals[sig].installed) {
		struct sigaction action;
		sigset_t sigset;
		sigemptyset(&sigset);
		action.sa_handler = sighandler;
		action.sa_mask = sigset;
		action.sa_flags = SA_NOCLDSTOP;
		sigaction(sig, &action, &all_signals[sig].oldact);
	}
	all_signals[sig].installed++;
}

static gboolean signal_prepare(GSource *source, gint *timeout)
{
	(void)source;
	*timeout = -1;
	return signal_fired;
}

static gboolean signal_check(GSource *source)
{
	(void)source;
	return signal_fired;
}

static gboolean signal_occurred(GSource *source, GSourceFunc callback, gpointer data)
{
	guint i;
	sigset_t oldset;
	guint fired[NUM_SIGNALS];
	(void)source;
	(void)callback;
	(void)data;
	sigprocmask(SIG_SETMASK, &all_signals_set, &oldset);
	for(i = 0; i < NUM_SIGNALS; ++i) {
		fired[i] = signals_fired[i];
		signals_fired[i] = 0;
	}
	signal_fired = FALSE;
	sigprocmask(SIG_SETMASK, &oldset, NULL);
	for(i = 0; i < NUM_SIGNALS; ++i) {
		while(fired[i]) {
			GSList *it;
			for(it = callbacks[i]; it; it = g_slist_next(it)) {
				const struct t_signal_callback *cb = it->data;
				cb->func(i, cb->data);
			}
			--fired[i];
		}
	}
	return TRUE;
}

static void sighandler(gint sig)
{
	guint i;
	g_return_if_fail(sig < NUM_SIGNALS);
	for(i = 0; i < NUM_CORE_SIGNALS; ++i)
		if(sig == core_signals[i]) {
			fprintf(stderr, "How are you gentlemen? All your base are belong to us. (lboxwm received signal %d)\n", sig);
			abort();
		}
	signal_fired = TRUE;
	++signals_fired[sig];
}
